/*
 * The contents of this file are subject to the Mozilla Public License
 * Version 1.1 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations
 * under the License.
 * 
 * The Original Code is iSQL-Viewer, A Mutli-Platform Database Tool.
 *
 * The Initial Developer of the Original Code is iSQL-Viewer, A Mutli-Platform Database Tool.
 * Portions created by Mark A. Kobold are Copyright (C) 2000-2007. All Rights Reserved.
 *
 * Contributor(s): 
 *  Mark A. Kobold [mkobold <at> isqlviewer <dot> com].
 *  
 * If you didn't download this code from the following link, you should check
 * if you aren't using an obsolete version: http://www.isqlviewer.com
 */
package org.isqlviewer.swing.outline;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.MouseEvent;
import java.util.Date;
import java.util.EventObject;
import java.util.Hashtable;

import javax.swing.JTable;
import javax.swing.JTree;
import javax.swing.ListSelectionModel;
import javax.swing.LookAndFeel;
import javax.swing.UIDefaults;
import javax.swing.UIManager;
import javax.swing.UIDefaults.ProxyLazyValue;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeSelectionModel;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;

import org.isqlviewer.ui.laf.EnhancedTreeUI;

/**
 * This example shows how to create a simple JTreeTable component, by using a JTree as a renderer (and editor) for the
 * cells in a particular column in the JTable.
 * 
 * @version 1.2 10/27/98
 * @author Philip Milne
 * @author Scott Violet
 */
public class JOutline extends JTable {

    private static final long serialVersionUID = -6011537580748076035L;
    /** A subclass of JTree. */
    protected TreeTableCellRenderer tree;

    public JOutline(OutlineModel treeTableModel) {

        super();

        // Create the tree. It will be used as a renderer and editor.
        tree = new TreeTableCellRenderer(treeTableModel);

        // Install a tableModel representing the visible rows in the tree.
        super.setModel(new TreeTableModelAdapter(treeTableModel, tree));

        // Force the JTable and JTree to share their row selection models.
        ListToTreeSelectionModelWrapper selectionWrapper = new ListToTreeSelectionModelWrapper();
        tree.setSelectionModel(selectionWrapper);
        setSelectionModel(selectionWrapper.getListSelectionModel());

        // Install the tree editor renderer and editor.
        setDefaultRenderer(OutlineModel.class, tree);
        setDefaultEditor(OutlineModel.class, new TreeTableCellEditor());

        // No grid.
        setShowGrid(false);

        // No intercell spacing
        setIntercellSpacing(new Dimension(0, 0));

        // And update the height of the trees row to match that of
        // the table.
        if (tree.getRowHeight() < 1) {
            // Metal looks better like this.
            setRowHeight(18);
        }

    }

    /**
     * Overridden to message super and forward the method to the tree. Since the tree is not actually in the component
     * hieachy it will never receive this unless we forward it in this manner.
     */
    @Override
    public void updateUI() {

        super.updateUI();
        if (tree != null) {
            tree.updateUI();
        }
        // Use the tree's default foreground and background colors in the
        // table.
        LookAndFeel.installColorsAndFont(this, "Tree.background", "Tree.foreground", "Tree.font");
    }

    /*
     * Workaround for BasicTableUI anomaly. Make sure the UI never tries to paint the editor. The UI currently uses
     * different techniques to paint the renderers and editors and overriding setBounds() below is not the right thing
     * to do for an editor. Returning -1 for the editing row in this case, ensures the editor is never painted.
     */
    @Override
    public int getEditingRow() {

        return (getColumnClass(editingColumn) == OutlineModel.class) ? -1 : editingRow;
    }

    /**
     * Overridden to pass the new rowHeight to the tree.
     */
    @Override
    public void setRowHeight(int rowHeight) {

        super.setRowHeight(rowHeight);
        if (tree != null && tree.getRowHeight() != rowHeight) {
            tree.setRowHeight(getRowHeight());
        }
    }

    /**
     * Returns the tree that is being shared between the model.
     */
    public JTree getTree() {

        return tree;
    }

    @Override
    protected void createDefaultRenderers() {

        defaultRenderersByColumnClass = new UIDefaults();

        // Numbers
        setLazyRenderer(Number.class, "org.isqlviewer.ui.laf.EnhancedNumberCellRenderer");
        // Doubles and Floats
        setLazyRenderer(Float.class, "org.isqlviewer.ui.laf.EnhancedDecimalCellRenderer");
        setLazyRenderer(Double.class, "org.isqlviewer.ui.laf.EnhancedDecimalCellRendererr");
        // Dates
        setLazyRenderer(Date.class, "org.isqlviewer.ui.laf.EnhancedDateCellRenderer");
        // Booleans
        setLazyRenderer(Boolean.class, "org.isqlviewer.ui.laf.EnhancedBooleanCellRenderer");
        // Everything else
        setLazyRenderer(Object.class, "org.isqlviewer.ui.laf.EnhancedTableCellRenderer");
    }

    private void setLazyValue(Hashtable<Class, ProxyLazyValue> h, Class c, String s) {

        h.put(c, new UIDefaults.ProxyLazyValue(s));
    }

    private void setLazyRenderer(Class c, String s) {

        setLazyValue(defaultRenderersByColumnClass, c, s);
    }

    /**
     * A TreeCellRenderer that displays a JTree.
     */
    public class TreeTableCellRenderer extends JTree implements TableCellRenderer {

        private static final long serialVersionUID = -1452280482504589192L;
        /** Last table/tree row asked to renderer. */
        protected int visibleRow;

        public TreeTableCellRenderer(TreeModel model) {

            super(model);
            setUI(new EnhancedTreeUI(JOutline.this));
        }

        /**
         * updateUI is overridden to set the colors of the Tree's renderer to match that of the table.
         */
        @Override
        public void updateUI() {

            super.updateUI();
            // Make the tree's cell renderer use the table's cell selection
            // colors.
            TreeCellRenderer tcr = getCellRenderer();
            if (tcr instanceof DefaultTreeCellRenderer) {
                DefaultTreeCellRenderer dtcr = ((DefaultTreeCellRenderer) tcr);
                // For 1.1 uncomment this, 1.2 has a bug that will cause an
                // exception to be thrown if the border selection color is
                // null.
                // dtcr.setBorderSelectionColor(null);
                dtcr.setTextSelectionColor(UIManager.getColor("Table.selectionForeground"));
                dtcr.setBackgroundSelectionColor(UIManager.getColor("Table.selectionBackground"));
            }
        }

        /**
         * Sets the row height of the tree, and forwards the row height to the table.
         */
        @Override
        public void setRowHeight(int rowHeight) {

            if (rowHeight > 0) {
                super.setRowHeight(rowHeight);
                if (JOutline.this != null && JOutline.this.getRowHeight() != rowHeight) {
                    JOutline.this.setRowHeight(getRowHeight());
                }
            }
        }

        /**
         * This is overridden to set the height to match that of the JTable.
         */
        @Override
        public void setBounds(int x, int y, int w, int h) {

            super.setBounds(x, 0, w, JOutline.this.getHeight());
        }

        /**
         * Sublcassed to translate the graphics such that the last visible row will be drawn at 0,0.
         */
        @Override
        public void paint(Graphics g) {

            g.translate(0, -visibleRow * getRowHeight());
            super.paint(g);
        }

        /**
         * TreeCellRenderer method. Overridden to update the visible row.
         */
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
                boolean hasFocus, int row, int column) {

            // if (isSelected)
            // setBackground(table.getSelectionBackground());
            // else
            // setBackground(table.getBackground());

            visibleRow = row;
            return this;
        }
    }

    /**
     * TreeTableCellEditor implementation. Component returned is the JTree.
     */
    public class TreeTableCellEditor extends AbstractCellEditor implements TableCellEditor {

        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int r, int c) {

            return tree;
        }

        /**
         * Overridden to return false, and if the event is a mouse event it is forwarded to the tree.
         * <p>
         * The behavior for this is debatable, and should really be offered as a property. By returning false, all
         * keyboard actions are implemented in terms of the table. By returning true, the tree would get a chance to do
         * something with the keyboard events. For the most part this is ok. But for certain keys, such as left/right,
         * the tree will expand/collapse where as the table focus should really move to a different column. Page up/down
         * should also be implemented in terms of the table. By returning false this also has the added benefit that
         * clicking outside of the bounds of the tree node, but still in the tree column will select the row, whereas if
         * this returned true that wouldn't be the case.
         * <p>
         * By returning false we are also enforcing the policy that the tree will never be editable (at least by a key
         * sequence).
         */
        @Override
        public boolean isCellEditable(EventObject e) {

            if (e instanceof MouseEvent) {
                for (int counter = getColumnCount() - 1; counter >= 0; counter--) {
                    if (getColumnClass(counter) == OutlineModel.class) {
                        MouseEvent me = (MouseEvent) e;
                        MouseEvent newME = new MouseEvent(tree, me.getID(), me.getWhen(), me.getModifiers(), me.getX()
                                - getCellRect(0, counter, true).x, me.getY(), me.getClickCount(), me.isPopupTrigger());
                        tree.dispatchEvent(newME);
                        break;
                    }
                }
            }
            return false;
        }
    }

    /**
     * ListToTreeSelectionModelWrapper extends DefaultTreeSelectionModel to listen for changes in the ListSelectionModel
     * it maintains. Once a change in the ListSelectionModel happens, the paths are updated in the
     * DefaultTreeSelectionModel.
     */
    class ListToTreeSelectionModelWrapper extends DefaultTreeSelectionModel {

        private static final long serialVersionUID = -2542037807820930938L;
        /** Set to true when we are updating the ListSelectionModel. */
        protected boolean updatingListSelectionModel;

        public ListToTreeSelectionModelWrapper() {

            super();
            getListSelectionModel().addListSelectionListener(createListSelectionListener());
        }

        /**
         * Returns the list selection model. ListToTreeSelectionModelWrapper listens for changes to this model and
         * updates the selected paths accordingly.
         */
        ListSelectionModel getListSelectionModel() {

            return listSelectionModel;
        }

        /**
         * This is overridden to set <code>updatingListSelectionModel</code> and message super. This is the only place
         * DefaultTreeSelectionModel alters the ListSelectionModel.
         */
        @Override
        public void resetRowSelection() {

            if (!updatingListSelectionModel) {
                updatingListSelectionModel = true;
                try {
                    super.resetRowSelection();
                } finally {
                    updatingListSelectionModel = false;
                }
            }
            // Notice how we don't message super if
            // updatingListSelectionModel is true. If
            // updatingListSelectionModel is true, it implies the
            // ListSelectionModel has already been updated and the
            // paths are the only thing that needs to be updated.
        }

        /**
         * Creates and returns an instance of ListSelectionHandler.
         */
        protected ListSelectionListener createListSelectionListener() {

            return new ListSelectionHandler();
        }

        /**
         * If <code>updatingListSelectionModel</code> is false, this will reset the selected paths from the selected
         * rows in the list selection model.
         */
        protected void updateSelectedPathsFromSelectedRows() {

            if (!updatingListSelectionModel) {
                updatingListSelectionModel = true;
                try {
                    // This is way expensive, ListSelectionModel needs an
                    // enumerator for iterating.
                    int min = listSelectionModel.getMinSelectionIndex();
                    int max = listSelectionModel.getMaxSelectionIndex();

                    clearSelection();
                    if (min != -1 && max != -1) {
                        for (int counter = min; counter <= max; counter++) {
                            if (listSelectionModel.isSelectedIndex(counter)) {
                                TreePath selPath = tree.getPathForRow(counter);

                                if (selPath != null) {
                                    addSelectionPath(selPath);
                                }
                            }
                        }
                    }
                } finally {
                    updatingListSelectionModel = false;
                }
            }
        }

        /**
         * Class responsible for calling updateSelectedPathsFromSelectedRows when the selection of the list changse.
         */
        class ListSelectionHandler implements ListSelectionListener {

            public void valueChanged(ListSelectionEvent e) {

                updateSelectedPathsFromSelectedRows();
            }
        }
    }

    /**
     * @param b
     */
    public void setRootVisible(boolean b) {

        tree.setRootVisible(b);
    }

    public void cancelEditing() {

        tree.cancelEditing();
    }

    public TreePath getPathForLocation(int x, int y) {

        return tree.getPathForLocation(x, y);
    }

    public int getRowForLocation(int x, int y) {

        return tree.getRowForLocation(x, y);
    }

    public void setTreeCellRenderer(TreeCellRenderer renderer) {

        tree.setCellRenderer(renderer);
    }

    public TreeCellRenderer getTreeCellRenderer() {

        return tree.getCellRenderer();
    }

    public Component getTreeCellRendererComponent(Object value, boolean selected, boolean expanded, boolean leaf,
            int row, boolean hasFocus) {

        TreeCellRenderer tcr = getTreeCellRenderer();
        return tcr.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
    }
}
